/*
* Copyright 2005, 2006, 2008 The Apache Software Foundation
* Copyright 2012 The MyBatis.org Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mybatis.generator.eclipse.core.merge;
import static org.mybatis.generator.eclipse.core.merge.EclipseDomUtils.getCompilationUnitFromSource;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.text.edits.TextEdit;
import org.mybatis.generator.eclipse.core.merge.InvalidExistingFileException.ErrorCode;
import org.mybatis.generator.exception.ShellException;
/**
* This class handles the task of merging changes into an existing Java file.
*
* This class makes several assumptions about the structure of the new and
* existing files, including:
*
* <ul>
* <li>The imports of both files are fully qualified (no wildcard imports)</li>
* <li>The super interfaces of both files are NOT fully qualified</li>
* <li>The super classes of both files are NOT fully qualified</li>
* </ul>
*
* @author Jeff Butler
* @author Tomas Neuberg
*/
public class JavaFileMerger {
private String newJavaSource;
private String existingJavaSource;
private String[] javaDocTags;
public JavaFileMerger(String newJavaSource, String existingJavaSource,
String[] javaDocTags) {
super();
this.newJavaSource = newJavaSource;
this.existingJavaSource = existingJavaSource;
this.javaDocTags = javaDocTags;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public String getMergedSource() throws ShellException, InvalidExistingFileException {
NewJavaFileVisitor newJavaFileVisitor = visitNewJavaFile();
IDocument document = new Document(existingJavaSource);
// delete generated stuff, and collect imports
ExistingJavaFileVisitor visitor = new ExistingJavaFileVisitor(
javaDocTags);
CompilationUnit cu = getCompilationUnitFromSource(existingJavaSource);
AST ast = cu.getAST();
cu.recordModifications();
cu.accept(visitor);
TypeDeclaration typeDeclaration = visitor.getTypeDeclaration();
if (typeDeclaration == null) {
throw new InvalidExistingFileException(ErrorCode.NO_TYPES_DEFINED_IN_FILE);
}
// reconcile the superinterfaces
List<Type> newSuperInterfaces = getNewSuperInterfaces(
typeDeclaration.superInterfaceTypes(), newJavaFileVisitor);
for (Type newSuperInterface : newSuperInterfaces) {
typeDeclaration.superInterfaceTypes().add(
ASTNode.copySubtree(ast, newSuperInterface));
}
// set the superclass
if (newJavaFileVisitor.getSuperclass() != null) {
typeDeclaration.setSuperclassType((Type) ASTNode.copySubtree(ast,
newJavaFileVisitor.getSuperclass()));
} else {
typeDeclaration.setSuperclassType(null);
}
// interface or class?
if (newJavaFileVisitor.isInterface()) {
typeDeclaration.setInterface(true);
} else {
typeDeclaration.setInterface(false);
}
// reconcile the imports
List<ImportDeclaration> newImports = getNewImports(cu.imports(),
newJavaFileVisitor);
for (ImportDeclaration newImport : newImports) {
Name name = ast
.newName(newImport.getName().getFullyQualifiedName());
ImportDeclaration newId = ast.newImportDeclaration();
newId.setName(name);
cu.imports().add(newId);
}
TextEdit textEdit = cu.rewrite(document, null);
try {
textEdit.apply(document);
} catch (BadLocationException e) {
throw new ShellException(
"BadLocationException removing prior fields and methods");
}
// regenerate the CompilationUnit to reflect all the deletes and changes
CompilationUnit strippedCu = getCompilationUnitFromSource(document.get());
// find the top level public type declaration
TypeDeclaration topLevelType = null;
Iterator iter = strippedCu.types().iterator();
while (iter.hasNext()) {
TypeDeclaration td = (TypeDeclaration) iter.next();
if (td.getParent().equals(strippedCu)
&& (td.getModifiers() & Modifier.PUBLIC) > 0) {
topLevelType = td;
break;
}
}
// now add all the new methods and fields to the existing
// CompilationUnit with a ListRewrite
ASTRewrite rewrite = ASTRewrite.create(topLevelType.getRoot().getAST());
ListRewrite listRewrite = rewrite.getListRewrite(topLevelType,
TypeDeclaration.BODY_DECLARATIONS_PROPERTY);
Iterator<ASTNode> astIter = newJavaFileVisitor.getNewNodes().iterator();
int i = 0;
while (astIter.hasNext()) {
ASTNode node = astIter.next();
if (node.getNodeType() == ASTNode.TYPE_DECLARATION) {
String name = ((TypeDeclaration) node).getName()
.getFullyQualifiedName();
if (visitor.containsInnerClass(name)) {
continue;
}
} else if (node instanceof FieldDeclaration) {
addExistsAnnotations((BodyDeclaration) node,
visitor.getFieldAnnotations((FieldDeclaration) node));
} else if (node instanceof MethodDeclaration) {
addExistsAnnotations((BodyDeclaration) node,
visitor.getMethodAnnotations((MethodDeclaration) node));
}
listRewrite.insertAt(node, i++, null);
}
textEdit = rewrite.rewriteAST(document, JavaCore.getOptions());
try {
textEdit.apply(document);
} catch (BadLocationException e) {
throw new ShellException(
"BadLocationException adding new fields and methods");
}
String newSource = document.get();
return newSource;
}
private List<Type> getNewSuperInterfaces(
List<Type> existingSuperInterfaces,
NewJavaFileVisitor newJavaFileVisitor) {
List<Type> answer = new ArrayList<Type>();
for (Type newSuperInterface : newJavaFileVisitor
.getSuperInterfaceTypes()) {
boolean found = false;
for (Type existingSuperInterface : existingSuperInterfaces) {
found = EclipseDomUtils.typesMatch(newSuperInterface,
existingSuperInterface);
if (found) {
break;
}
}
if (!found) {
answer.add(newSuperInterface);
}
}
return answer;
}
private List<ImportDeclaration> getNewImports(
List<ImportDeclaration> existingImports,
NewJavaFileVisitor newJavaFileVisitor) {
List<ImportDeclaration> answer = new ArrayList<ImportDeclaration>();
for (ImportDeclaration newImport : newJavaFileVisitor.getImports()) {
boolean found = false;
for (ImportDeclaration existingImport : existingImports) {
found = EclipseDomUtils.importDeclarationsMatch(newImport,
existingImport);
if (found) {
break;
}
}
if (!found) {
answer.add(newImport);
}
}
return answer;
}
/**
* This method parses the new Java file and returns a filled out
* NewJavaFileVisitor. The returned visitor can be used to determine
* characteristics of the new file, and a lost of new nodes that need to be
* incorporated into the existing file.
*
* @return
*/
private NewJavaFileVisitor visitNewJavaFile() {
CompilationUnit cu = getCompilationUnitFromSource(newJavaSource);
NewJavaFileVisitor newVisitor = new NewJavaFileVisitor();
cu.accept(newVisitor);
return newVisitor;
}
@SuppressWarnings("unchecked")
private void addExistsAnnotations(BodyDeclaration node,
List<Annotation> oldAnnotations) {
Set<String> newAnnotationTypes = new HashSet<String>();
int lastAnnotationIndex = 0;
int idx = 0;
for (Object modifier : node.modifiers()) {
if (modifier instanceof Annotation) {
Annotation newAnnotation = (Annotation) modifier;
newAnnotationTypes.add(newAnnotation.getTypeName()
.getFullyQualifiedName());
lastAnnotationIndex = idx;
}
idx++;
}
if (oldAnnotations != null) {
for (Annotation oldAnnotation : oldAnnotations) {
if (newAnnotationTypes.contains(oldAnnotation.getTypeName()
.getFullyQualifiedName()))
continue;
AST nodeAst = node.getAST();
node.modifiers().add(lastAnnotationIndex++,
ASTNode.copySubtree(nodeAst, oldAnnotation));
}
}
}
}